<!DOCTYPE HTML>
<html>
<head>
	<meta charset="utf-8">
	<link href="styles.css" rel="stylesheet">
	<title>Peak drop equation analysis | audioMotion-analyzer</title>
</head>
<body>
	<div class="container">
		<div>
			<h1>audioMotion-analyzer</h1>
			<h2>peak drop equation analysis</h2>
			<canvas id="canvas" width="400" height="400"></canvas>
			<div id="legend"></div>
			<div class="note">Click labels to show / hide the corresponding curves</div>
			<div class="grid grid-3-cols">
				<div>Time range (ms): <input id="time-range" type="number" min="500" max="5000" step="50" value="1000"></div>
				<div>Frame rate (fps): <input id="frame-rate" type="number" min="10" max="240" step="1" value="60"></div>
				<div>gravity (kpx/s²): <input id="user-gravity" type="number" min=".1" max="30" step=".1" value="2.1"></div>
				<div>Analyzer height (px): <input id="canvas-height" type="number" min="270" max="2160" step="1" value="1080"></div>
				<div></div>
				<button>Refresh</button>
			</div>
			<p></p>
			<div class="note margin-top">
				<table>
					<tr><th>version</th><th>gravity (peak acceleration)</th><th>decay time from max to zero</th></tr>
					<tr><td><span class="bullet round" style="background: #000000"></span>v1.0.0</td><td>varies with frame rate (60fps optimal)</td><td>varies with analyzer height</td></tr>
					<tr><td><span class="bullet round" style="background: #039aff"></span>v3.5.0</td><td>varies with analyzer height</td><td>varies with frame rate</td></tr>
					<tr><td><span class="bullet round" style="background: #ffd800"></span>v4.2.0</td><td>varies with analyzer height</td><td>constant</td></tr>
					<tr><td><span class="bullet round" style="background: #ff3500"></span>v4.5.0</td><td>constant (customizable)</td><td>varies with analyzer height</td></tr>
				</table>
			</div>
			<p class="credits absolute-center">
				<strong>audioMotion-analyzer</strong> Copyright &copy; 2018-2025 Henrique Avila Vianna.<br>
				Licensed under AGPL-3.0 or later. Source code is available on <a href="https://github.com/hvianna/audioMotion-analyzer">GitHub</a>.
			</p>
		</div>
	</div>

<script>
const canvas = document.getElementById('canvas'),
	  legend = document.getElementById('legend'),
	  ctx    = canvas.getContext('2d'),
	  colors = [ '#000000', '#039aff', '#ffd800', '#ff3500', '#00aa2e' ],
	  labels = [
			{ val: 'v100', text: 'v1.0.0' },
		  	{ val: 'v350', text: 'v3.5.0' },
		  	{ val: 'v420', text: 'v4.2.0' },
			{ val: 'v450', text: 'v4.5.0 (default)' },
			{ val: 'user', text: 'custom gravity: <span id="label-gravity"></span>' }
	  ];

function buildGraph() {

	const GRAPH_BORDER = 50,
		  GRAPH_HEIGHT = 400,
		  GRAPH_WIDTH  = 800;

	// peak decay emulation settings

	const canvasHeight    = +document.getElementById('canvas-height').value,
		  frameRate       = +document.getElementById('frame-rate').value,
		  DEFAULT_GRAVITY = 3.8;

	// This function simulates peak drop - `hold` is bar.hold[ channel ] in the `_draw()` method
	// Returns an array of peak value for each frame

	const drop = ( callback, peakValue, gravity ) => {
		let ret = [ peakValue ];
		// hold decreases by 1 on every animation frame and peak decay starts when hold < 0
		for ( let hold = -1; peakValue > 0 && hold > frameRate * -10; hold-- ) {
			peakValue += callback( hold, gravity );
			ret.push( peakValue );
		}
		return ret;
	}

	// callbacks provide the decay equations used in different versions of audioMotion-analyzer

	const callbacks = {
		// v4.5.0
		// 1 to 0 in ~750ms using default gravity and canvas height = 1080px
		v450: ( hold, gravity ) => ( hold * gravity * 1e3 / frameRate ** 2 ) / canvasHeight,

		// v4.2.0 to 4.4.0
		// 1 to 0 in ~500ms - frame rate independent
		v420: hold => {
			const holdFrames = frameRate >> 1;
			return hold / ( holdFrames * holdFrames / 2 );
		},

		// v3.5.0 to 4.1.1 (peak value normalized [0;1])
		// 1 to 0 in ~45 frames (~750ms @60fps)
		v350: hold => hold / 1080,

		// v1.0.0 to 3.4.0 (peak value in pixels)
		// 1080 to 0 in 45 frames (750ms @60fps)
		v100: hold => hold - 1
	}

	// generate graph

	const maxX   = GRAPH_WIDTH - GRAPH_BORDER,
		  minX   = GRAPH_BORDER,
		  maxY   = GRAPH_HEIGHT - GRAPH_BORDER,
		  minY   = GRAPH_BORDER,
		  rangeX = +document.getElementById('time-range').value,
		  rangeY = 1,
		  graphX = val => minX + ( maxX - minX ) / rangeX * val,
		  graphY = val => maxY - ( maxY - minY ) / rangeY * val;

	const userGravity = +document.getElementById('user-gravity').value;
	document.getElementById('label-gravity').innerText = userGravity; // update graph label with custom gravity

	canvas.width  = GRAPH_WIDTH;
	canvas.height = GRAPH_HEIGHT;

	ctx.lineWidth = 2;
	ctx.moveTo( minX - 1, minY - 1 );
	ctx.lineTo( minX - 1, maxY + 1 );
	ctx.lineTo( maxX + 1, maxY + 1 );
	ctx.stroke();

	ctx.lineWidth = 1.5;
	ctx.strokeStyle = '#ccc';
	ctx.font = '14px sans-serif';

	ctx.textAlign = 'right';
	ctx.beginPath();
	for ( let valY = 1; valY >= 0; valY -= .25 ) {
		const y = graphY( valY ) | 0;
		ctx.fillText( valY, minX - 7, y + 5 );
		if ( valY > 0 ) {
			ctx.moveTo( minX, y );
			ctx.lineTo( maxX, y );
		}
	}

	ctx.textAlign = 'center';
	ctx.fillText( 'Time (ms)', canvas.width / 2, canvas.height - 5 );

	ctx.save();
	ctx.rotate( -Math.PI / 2 );
	ctx.fillText( 'Peak value', -canvas.height / 2, 10 );
	ctx.restore();

	for ( let valX = 0; valX <= rangeX; valX += rangeX / 10 ) {
		const x = graphX( valX );
		if ( valX % ( rangeX / 10 ) == 0 )
			ctx.fillText( valX, x, maxY + 20 );
		if ( valX > 0 ) {
			ctx.moveTo( x, minY );
			ctx.lineTo( x, maxY );
		}
	}
	ctx.stroke();

	// plot curves

	ctx.lineWidth = 2.5;
	for ( let i = 0; i < labels.length; i++ ) {
		const label = labels[ i ].val;
		if ( document.getElementById(`label-${ label }`).checked ) {
			ctx.beginPath();
			ctx.strokeStyle = colors[ i ] + 'cc';

			const peakValue = label == 'v100' ? canvasHeight : 1,
				  gravity   = label == 'user' ? userGravity : DEFAULT_GRAVITY,
				  data      = drop( callbacks[ label == 'user' ? 'v450' : label ], peakValue, gravity );

			for ( const [ index, val ] of data.entries() ) {
				const method = index == 0 ? 'moveTo' : 'lineTo',
					  time   = index / frameRate * 1000;
				ctx[ method ]( graphX( time ), graphY( val / ( label == 'v100' ? canvasHeight : 1 ) ) );
			}
			ctx.stroke();
		}
	}

}

/* main */

for ( let i = 0; i < labels.length; i++ )
	legend.innerHTML += `<label class="labelSelector"><input type="checkbox" id="label-${ labels[ i ].val }" checked> ` +
						`<span class="bullet" style="background: ${ colors[ i ] }"></span> ${ labels[ i ].text }</label>`;

for ( const el of document.querySelectorAll('input[id^="label-"]') )
	el.addEventListener( 'click', () => buildGraph() );

for ( const el of document.querySelectorAll('input[type="number"]') )
	el.addEventListener( 'change', () => buildGraph() );

buildGraph();

</script>

</body>
</html>
